import 'package:hive_ce_flutter/hive_flutter.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';

import 'package:flutter_cache_manager/src/storage/cache_info_repositories/cache_info_repository.dart';
import 'package:flutter_cache_manager/src/storage/cache_info_repositories/helper_methods.dart';
import 'package:flutter_cache_manager/src/storage/cache_object.dart';

const _tableCacheObject = 'cacheObject';

class CacheObjectProvider extends CacheInfoRepository with CacheInfoRepositoryHelperMethods {
  String? databaseName;
  late IsolatedBox<Map> _box;
  late IsolatedBox<int> _indexBox;

  String get _boxName => databaseName ?? _tableCacheObject;
  String get _indexBoxName => '${_boxName}_index';

  /// Either the path or the database name should be provided.
  /// If the path is provider it should end with '{databaseName}.db',
  /// for example: /data/user/0/com.example.example/databases/imageCache.db
  CacheObjectProvider({String? path, this.databaseName});

  @override
  Future<bool> open() async {
    if (!shouldOpenOnNewConnection()) {
      return openCompleter!.future;
    }

    final appDir = await getApplicationDocumentsDirectory();
    final dbPath = join(appDir.path, 'file_cache_manager');
    _box = await IsolatedHive.openBox<Map>(_boxName, path: dbPath);
    _indexBox = await IsolatedHive.openBox<int>(_indexBoxName, path: dbPath);
    return opened();
  }

  @override
  Future<dynamic> updateOrInsert(CacheObject cacheObject) {
    if (cacheObject.id == null) {
      return insert(cacheObject);
    } else {
      return update(cacheObject);
    }
  }

  @override
  Future<CacheObject> insert(CacheObject cacheObject, {bool setTouchedToNow = true}) async {
    final map = cacheObject.toMap(setTouchedToNow: setTouchedToNow);
    // Add without id, Hive will return an int key
    final int id = await _box.add(map);
    // Update stored map with id for compatibility
    final stored = Map<String, dynamic>.from(map);
    stored[CacheObject.columnId] = id;
    await _box.put(id, stored);
    // maintain index for quick lookup by key
    final key = stored[CacheObject.columnKey] as String? ?? stored[CacheObject.columnUrl] as String;
    await _indexBox.put(key, id);
    return cacheObject.copyWith(id: id);
  }

  @override
  Future<CacheObject?> get(String key) async {
    // Try index first
    final id = await _indexBox.get(key);
    if (id != null) {
      final map = (await _box.get(id))?.cast<String, dynamic>();
      if (map != null) return CacheObject.fromMap(map);
    }
    // Fallback: full scan
    for (final entry in (await _box.toMap()).entries) {
      final map = (entry.value).cast<String, dynamic>();
      if ((map[CacheObject.columnKey] as String? ?? map[CacheObject.columnUrl] as String) == key) {
        // update index
        await _indexBox.put(key, entry.key as int);
        return CacheObject.fromMap(map);
      }
    }
    return null;
  }

  @override
  Future<int> delete(int id) async {
    final map = (await _box.get(id))?.cast<String, dynamic>();
    if (map != null) {
      final key = map[CacheObject.columnKey] as String? ?? map[CacheObject.columnUrl] as String;
      final indexed = await _indexBox.get(key);
      if (indexed == id) await _indexBox.delete(key);
    }
    return _box.delete(id).then((_) => 1);
  }

  @override
  Future<int> deleteAll(Iterable<int> ids) {
    return _box.deleteAll(ids.toList()).then((_) => ids.length);
  }

  @override
  Future<int> update(CacheObject cacheObject, {bool setTouchedToNow = true}) {
    final map = cacheObject.toMap(setTouchedToNow: setTouchedToNow);
    final id = cacheObject.id!;
    final stored = Map<String, dynamic>.from(map);
    stored[CacheObject.columnId] = id;
    _indexBox.put(stored[CacheObject.columnKey] as String? ?? stored[CacheObject.columnUrl] as String, id);
    return _box.put(id, stored).then((_) => 1);
  }

  @override
  Future<List<CacheObject>> getAllObjects() async {
    final list = <Map<String, dynamic>>[];
    for (final v in (await _box.values)) {
      list.add((v).cast<String, dynamic>());
    }
    return CacheObject.fromMapList(list);
  }

  @override
  Future<List<CacheObject>> getObjectsOverCapacity(int capacity) async {
    final cutoff = DateTime.now().subtract(const Duration(days: 1)).millisecondsSinceEpoch;
    final list = <Map<String, dynamic>>[];
    for (final v in (await _box.values)) {
      final map = (v).cast<String, dynamic>();
      final touched = (map[CacheObject.columnTouched] as int?) ?? 0;
      if (touched < cutoff) list.add(map);
    }
    list.sort((a, b) => (b[CacheObject.columnTouched] as int).compareTo(a[CacheObject.columnTouched] as int));
    if (capacity >= list.length) return [];
    final sliced = list.skip(capacity).take(100).toList();
    return CacheObject.fromMapList(sliced);
  }

  @override
  Future<List<CacheObject>> getOldObjects(Duration maxAge) async {
    final cutoff = DateTime.now().subtract(maxAge).millisecondsSinceEpoch;
    final list = <Map<String, dynamic>>[];
    for (final v in (await _box.values)) {
      final map = (v).cast<String, dynamic>();
      final touched = (map[CacheObject.columnTouched] as int?) ?? 0;
      if (touched < cutoff) list.add(map);
    }
    list.sort((a, b) => (b[CacheObject.columnTouched] as int).compareTo(a[CacheObject.columnTouched] as int));
    return CacheObject.fromMapList(list.take(100).toList());
  }

  @override
  Future<bool> close() async {
    if (!shouldClose()) return false;
    await _box.close();
    await _indexBox.close();
    return true;
  }

  @override
  Future<void> deleteDataFile() async {
    await open();

    await _box.deleteFromDisk();
    await _indexBox.deleteFromDisk();
  }

  @override
  Future<bool> exists() {
    return open();
  }
}
